﻿/*
BF4Rcon.NET, provides a .NET implementation of BF4's RCON interface.
Copyright (C) 2013 Tim Marro

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

//will want to clean this up later. was in rush to write it

namespace TimScripts.BF4Rcon.Frostbite
{
    internal static class MD5
    {
        private static readonly uint[] Table = GenerateTable();
        private static readonly int[] ShiftAmounts = 
        {
            7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
            5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
            4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
            6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
        };

        //public static string GenerateHash(string input)
        //{
        //    byte[] inputBytes = Encoding.UTF8.GetBytes(input);

        //    string result = GenerateHash(inputBytes);

        //    return result;
        //}

        public static string GenerateHash(byte[] inputBytes)
        {
            uint[] hash = { 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476 };
            byte[] resultBytes;
            uint offset = 0;
            int numFullBlocks = inputBytes.Length / 64;
            int remainingData = inputBytes.Length % 64;
            StringBuilder sb = new StringBuilder();

            for (int i = 0; i < numFullBlocks; i++)
            {
                CalculateBlock(inputBytes, offset, hash);

                offset += 64;
            }

            if (remainingData < 56)
            {
                CalculateLastBlock(inputBytes, (int)offset, hash);
            }
            else
            {
                CalculateLastTwoBlocks(inputBytes, (int)offset, hash);
            }

            resultBytes = ConvertHash(hash);

            foreach (byte b in resultBytes)
            {
                sb.AppendFormat("{0:x2}", b);
            }

            return sb.ToString();
        }

        //repeated code but i sick and dont care
        private static void CalculateLastTwoBlocks(byte[] inputBytes, int offset, uint[] hash)
        {
            byte[] finalBlocks = new byte[128];
            byte[] lengthBytes = GetBytes(8ul * (ulong)inputBytes.Length);
            int copyLength = inputBytes.Length - offset;

            Buffer.BlockCopy(inputBytes, offset, finalBlocks, 0, copyLength);

            finalBlocks[copyLength] = 0x80;

            Buffer.BlockCopy(lengthBytes, 0, finalBlocks, finalBlocks.Length - lengthBytes.Length, lengthBytes.Length);

            for (uint i = 0; i < 2; i++)
            {
                CalculateBlock(finalBlocks, 64u * i, hash);
            }
        }

        private static void CalculateLastBlock(byte[] inputBytes, int offset, uint[] hash)
        {
            byte[] lastBlock = new byte[64];
            byte[] lengthBytes = GetBytes(8ul * (ulong)inputBytes.Length); ;
            int length = inputBytes.Length - offset;

            Buffer.BlockCopy(inputBytes, offset, lastBlock, 0, length);

            lastBlock[length] = 0x80;

            Buffer.BlockCopy(lengthBytes, 0, lastBlock, lastBlock.Length - lengthBytes.Length, lengthBytes.Length);

            CalculateBlock(lastBlock, 0, hash);
        }

        private static byte[] ConvertHash(uint[] hash)
        {
            byte[] result = new byte[16];
            byte[] wordBytes;

            for (int i = 0; i < hash.Length; i++)
            {
                wordBytes = GetBytes(hash[i]);

                Buffer.BlockCopy(wordBytes, 0, result, wordBytes.Length * i, wordBytes.Length);
            }

            return result;
        }

        private static byte[] GetBytes(uint p)
        {
            byte[] bytes = BitConverter.GetBytes(p);
            if (!BitConverter.IsLittleEndian) Array.Reverse(bytes);

            return bytes;
        }

        private static byte[] GetBytes(ulong p)
        {
            byte[] bytes = BitConverter.GetBytes(p);
            if (!BitConverter.IsLittleEndian) Array.Reverse(bytes);

            return bytes;
        }

        private static void CalculateBlock(byte[] inputBytes, uint offset, uint[] hash)
        {
            uint tempA = hash[0];
            uint tempB = hash[1];
            uint tempC = hash[2];
            uint tempD = hash[3];

            uint t;
            uint funcVal;
            uint wordIndex = 0;

            for (uint i = 0; i < 64; i++)
            {
                if (i < 16)
                {
                    funcVal = Round1(tempB, tempC, tempD, i, ref wordIndex);
                }
                else if (i < 32)
                {
                    funcVal = Round2(tempB, tempC, tempD, i, ref wordIndex);
                }
                else if (i < 48)
                {
                    funcVal = Round3(tempB, tempC, tempD, i, ref wordIndex);
                }
                else
                {
                    funcVal = Round4(tempB, tempC, tempD, i, ref wordIndex);
                }

                t = tempD;
                tempD = tempC;
                tempC = tempB;
                tempB = tempB + LeftRotate(tempA + funcVal + Table[i] + GetWord(inputBytes, offset, wordIndex), ShiftAmounts[i]);
                tempA = t;
            }

            hash[0] += tempA;
            hash[1] += tempB;
            hash[2] += tempC;
            hash[3] += tempD;
        }

        private static uint LeftRotate(uint p1, int p2)
        {
            return (p1 << p2) | (p1 >> (32 - p2));
        }

        private static uint GetWord(byte[] inputBytes, uint offset, uint wordIndex)
        {
            uint result = 0;

            offset += sizeof(uint) * wordIndex;

            for (int i = 0; i < 4; i++)
            {
                result |= (uint)inputBytes[offset + i] << (8 * i);
            }

            return result;
        }

        private static uint Round4(uint b, uint c, uint d, uint i, ref uint wordIndex)
        {
            wordIndex = (7 * i) % 16;

            return c ^ (b | ~d);
        }

        private static uint Round3(uint b, uint c, uint d, uint i, ref uint wordIndex)
        {
            wordIndex = (3 * i + 5) % 16;

            return b ^ c ^ d;
        }

        private static uint Round2(uint b, uint c, uint d, uint i, ref uint wordIndex)
        {
            wordIndex = (5 * i + 1) % 16;

            return (d & b) | (~d & c);
        }

        private static uint Round1(uint b, uint c, uint d, uint i, ref uint wordIndex)
        {
            wordIndex = i;

            return (b & c) | (~b & d);
        }

        private static uint[] GenerateTable()
        {
            uint[] result = new uint[64];

            for (int i = 0; i < 64; i++)
            {
                result[i] = (uint)(Math.Abs(Math.Sin(i + 1)) * Math.Pow(2, 32));
            }

            return result;
        }
    }
}
